package com.raidan.dclog.core.render.queries;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.raidan.dclog.core.Config;
import com.raidan.dclog.core.Const;
import com.raidan.dclog.core.FileUtils;
import com.raidan.dclog.core.Utils;

/**
 * Class for loading template for queries. Templates defined by simple text
 * files (like an XML structure).
 * 
 * @author raidan
 * 
 */
public class QueryLoader {

	private QueryLoader() {
		// 
	}

	private final static Logger logger = LoggerFactory.getLogger(QueryLoader.class);

	private static QueryLoader instance = null;

	public static synchronized QueryLoader getInstance() throws IOException {
		if (instance == null) {
			instance = new QueryLoader();
			instance.loadQueries();
		}
		return instance;
	}

	/**
	 * Основной метод для загрузки встроенных отчетов
	 * 
	 * @todo: Добавить загрузку пользовательских отчетов
	 * 
	 * @throws IOException
	 */
	protected void loadQueries() throws IOException {
		this.loadQueries("com/raidan/dclog/core/render/queries/");
	}

	private HashMap<String, Query> queries = null;
	private HashMap<String, Query> cycleTemplates = null;

	private final static String CYCLE_NAME_END = "-cycle";

	/**
	 * Основной метод для загрузки встроенных отчетов
	 * 
	 * @param path
	 *            Путь к шаблонам. Не забывайте заканчивать этот путь слэшем!
	 * @throws IOException
	 */
	protected void loadQueries(String path) throws IOException {
		queries = new LinkedHashMap<String, Query>();
		cycleTemplates = new HashMap<String, Query>();
		String files[] = FileUtils.listFilesFromJAR(path, ".template");

		// В дальнейшем легко можно модифицировать этот код так, чтобы
		// пропускать все ошибочные шаблоны
		for (String name : files) {
			String fileBody = FileUtils.loadFromJAR(name).toString();
			String templateName = name.substring(name.lastIndexOf("/") + 1, name.lastIndexOf(".template"));

			// Разрешенные значения в тэгах -- all и cycle
			String skip = Utils.getFirstBetweenTags(fileBody, "skip");

			if ("all".equals(skip)) {
				logger.debug("Total skipping template " + templateName + "...");
				continue; //
			}

			Query q = new Query();
			q.setName(templateName);
			q.setSystem(true);
			q.setDescription(Utils.getFirstBetweenTags(fileBody, "desc"));
			q.setSql(Utils.getFirstBetweenTags(fileBody, "query"));

			if (q.getSql() == null || q.getSql().length() == 0) {
				throw new InvalidTemplateException("Template '" + q.getName()
						+ "' has an empty SQL. This is not allowed.");
			}
			if (q.getDescription() == null || q.getDescription().length() == 0) {
				throw new InvalidTemplateException("Template '" + q.getName()
						+ "' has an empty DESCRIPTION. This is not allowed.");
			}

			boolean isCycleTemplate = q.getName().endsWith(CYCLE_NAME_END);

			String cycle = Utils.getFirstBetweenTags(fileBody, "cycle");
			if (cycle != null) { // #1

				if ("cycle".equals(skip)) {
					logger.debug("Skipping cycle for template " + templateName + "...");
				} else {

					if (isCycleTemplate) {
						throw new InvalidCycleOverCycleException("Cycle template '" + q.getName()
								+ "' depends on cycle is prohibited.");
					}

					q.setCycle(true);
					q.setCycleColumn(Utils.getFirstBetweenTags(cycle, "row"));
					q.setCycleTemplate(Utils.getFirstBetweenTags(cycle, "template"));
				}

			}// #1 if

			// Time for check SQL
			if (isCycleTemplate) {
				if (q.getSql().indexOf(Const.CYCLE_MACROS) < 0) {
					throw new InvalidMacrosDefinitionException("Template '" + q.getName()
							+ "' is template for cycle but " + "does not have " + Const.CYCLE_MACROS
							+ " mascros in SQL body.");
				}
			}
			// No, I don't want to give parameters into SQL query and use
			// any bind-variables
			String macrosList[] = Utils.getMacrosList(q.getSql());
			if (macrosList != null) {
				for (String macro : macrosList) {
					logger.trace("Processing macro " + macro + " for template " + q.getName() + "...");

					String fullMacroName = Const.MACROS_DEFINITION + macro + Const.MACROS_DEFINITION;
					if (Const.CYCLE_MACROS.equals(fullMacroName)) {
						if (!isCycleTemplate) {
							throw new InvalidMacrosDefinitionException("Template '" + q.getName() + "' is have "
									+ Const.CYCLE_MACROS
									+ " mascros, but this macros can be placed only in cycle templates.");
						}
						// This macros we'll not process anyway
						continue; //
					}

					String value = Config.getInstance().getQueryMacrosValue(q.getName(), macro);
					if (value == null) {
						throw new InvalidMacrosDefinitionException("Template '" + q.getName() + "' has macros '"
								+ macro + "', but this macros does not defined in properties. "
								+ "Unable to load template.");
					}
					q.setSql(Utils.replaceAll(q.getSql(), fullMacroName, value));
				}
			}

			if (isCycleTemplate) {
				cycleTemplates.put(q.getName(), q);
			} else {
				queries.put(q.getName(), q);
			}

		}

		for (Query q : this.queries.values()) {
			if (q.isCycle() && cycleTemplates.get(q.getCycleTemplate()) == null) {
				throw new InvalidCycleDependentException("Template '" + q.getName() + "' request cycle template '"
						+ q.getCycleTemplate() + "', but this template is not found.");
			}
		}

		// @todo: сделать проверку ссылок в cycle-шаблонах
	}

	@SuppressWarnings("unchecked")
	public Map<String, Query> getQueries() {
		return (Map<String, Query>) this.queries.clone();
	}

	@SuppressWarnings("unchecked")
	public Map<String, Query> getCycleTemplates() {
		return (Map<String, Query>) this.cycleTemplates.clone();
	}

}
